Read live bot state with the engine's own edict dump - #261
Conversation
Closes the adopt half of #255. ED_PrintEdicts walks the progs field definitions, so `edicts` dumps every entity with all its non-default fields, ours included. Confirmed mid-match on dm4 at t 25.1: EDICT 231 Carmack ar_mode 2 ar_node entity 91 ar_goal entity 223 ar_enemy entity 237 health 72 EDICT 234 Romero ar_node entity 53 ar_goal entity 219 ar_routegoal entity 219 ar_laststart entity 39 EDICT 237 Joe Rogan ar_mode 2 ar_node entity 101 ar_enemy entity 231 That is the state every forensics session this week added a dprint and recompiled to read. The door freeze needed ar_door and ar_liftwait. The routefail storm needed ar_goal and ar_failstreak. #257's 144 s freeze still needs ar_liftwait and ar_hoptrain. All of it was one console command away the whole time. TWO PARTS. The tune whitelist admits edicts, edict <n> and edictcount, so they can be injected into a live match through the channel that already exists. All three only print, so they are as safe as status, which was already allowed. `edict all` still rejects. tools/argus_edicts.py turns a dump back into something readable: edict pressure against the 600 ceiling, per-bot state in the order you want it when something is stuck, --field to compare one value across bots, --edict for one entity in full. With no injection channel, --make-cfg writes a config of `wait` lines and a trailing edicts for +exec, which is how the dm4 capture above was taken. THE DUMP IS LOSSY, which cost an hour and is worth knowing. A 236 edict dm4 dump lost four EDICT headers to the console. A free edict prints FREE and no fields, so a stray FREE after a populated block belongs to a header that never arrived; claiming it marked a live bot as free and hid Romero entirely. The parser now attributes FREE only to an empty block and reports how many headers went missing. Field values are intact, counts are approximate. Not adopted here, and recorded on #255 with measurements: host_timescale runs 2.9x but drops engagements per game minute from 23.4 to 7.0, and sys_ticrate does nothing. Savegames are refused outright in multiplayer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02c91eed38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| h = HEAD.match(line) | ||
| if h: | ||
| cur = int(h.group(1)) | ||
| out.setdefault(cur, {}) |
There was a problem hiding this comment.
Reset an edict when a later dump repeats its header
When a persistent match log contains an initial edicts dump followed by the newly supported edict <n> probe, the repeated header reuses the existing dictionary. Because the engine omits fields whose current value is zero, old values such as ar_liftwait, ar_goal, or ar_enemy survive and are reported as current; if the edict is now FREE, it is likewise mistaken for a dropped header. Replace the record at a repeated header, or scope parsing to the latest requested probe, rather than merging snapshots.
Useful? React with 👍 / 👎.
| out.setdefault("lost", 0) | ||
| out["lost"] += 1 |
There was a problem hiding this comment.
Include headerless free edicts in the pressure totals
When a FREE record loses its header, this branch increments lost but discards the entity entirely, while the summary calculates total and free counts only from eds. The checked-in dm4 capture consequently reports 236 edicts dumped, 236 in use, 0 free, even though its four headerless FREE records mean 240 total, 236 used, and 4 free. Since the default output is intended to report edict pressure, retain placeholder records or incorporate lost into the totals.
Useful? React with 👍 / 👎.
| // as status. | ||
| Regex::new( | ||
| r"(?i)^(skill\s+[0-3]|fraglimit\s+\d{1,3}|timelimit\s+\d{1,3}|developer\s+[01]|deathmatch\s+1|map\s+[A-Za-z0-9_]+|scratch[1-4]\s+-?\d{1,6}|status|serverinfo)$", | ||
| r"(?i)^(skill\s+[0-3]|fraglimit\s+\d{1,3}|timelimit\s+\d{1,3}|developer\s+[01]|deathmatch\s+1|map\s+[A-Za-z0-9_]+|scratch[1-4]\s+-?\d{1,6}|status|serverinfo|edicts|edictcount|edict\s+\d{1,4})$", |
There was a problem hiding this comment.
Advertise the new commands in the MCP tune contract
Adding these alternatives only to the validator leaves the MCP-facing TuneArgs schema in server.rs and the rejection message in validate_tune listing the old command set, while knobs() also omits the dump commands. MCP clients and their models use that schema/help to determine valid inputs, so the new live-state workflow remains undiscoverable unless callers already know its implementation details; update those public descriptions alongside the whitelist.
Useful? React with 👍 / 👎.
Closes the adopt half of #255.
ED_PrintEdictswalks the progs field definitions, soedictsdumps every entity with all its non-default fields, ours included. Confirmed mid-match on dm4 at t 25.1:That is the state every forensics session this week added a dprint and recompiled to read. The door freeze needed
ar_doorandar_liftwait. The routefail storm neededar_goalandar_failstreak. #257's 144 s freeze still needsar_liftwaitandar_hoptrain. All of it was one console command away the whole time.Two parts
The tune whitelist admits
edicts,edict <n>andedictcount, so they can be injected into a live match through the channel that already exists. All three only print, so they are as safe asstatus, which was already allowed.edict allstill rejects.tools/argus_edicts.pyturns a dump back into something readable:--field ar_node--edict 234--make-cfg 25waitlines and a trailingedicts, for+execwhen there is no injection channelThe dm4 capture above was taken with
--make-cfg.The dump is lossy, which is worth knowing
A 236 edict dm4 dump lost four
EDICTheaders to the console. A free edict printsFREEand no fields, so a strayFREEafter a populated block belongs to a header that never arrived. Claiming it marked a live bot as free and hid Romero from the output entirely, which cost an hour to find.The parser now attributes
FREEonly to an empty block and reports how many headers went missing. Field values are intact; counts are approximate.Not adopted
Recorded on #255 with measurements.
host_timescaleruns 2.9x but drops engagements per game minute from 23.4 to 7.0, so tapes would not compare to any baseline.sys_ticratedoes nothing. Savegames are refused outright in multiplayer.121 lab tests pass.
🤖 Generated with Claude Code